﻿using System;
using System.Collections.Generic;
using System.Linq;
using Fabrication.Exceptions;
using Fabrication.Tests.TestClasses;
using NUnit.Framework;

namespace Fabrication.Tests
{
    [TestFixture]
    public class link_rules
    {
        [Test]
        public void should_handle_linking_to_generated_property_value()
        {
            var builtItem = new FabricationScope().Setup<DoublePropertyClass<string>>()
                .For(x => x.FirstValue).GenerateFullName()
                .Link(x => x.SecondValue).To(x => x.FirstValue)
                .Generate();

            Assert.That(builtItem.SecondValue, Is.EqualTo(builtItem.FirstValue));
        }

        [Test]
        public void should_handle_linking_when_generating_multiple_items()
        {
            var builtItems = new FabricationScope().Setup<DoublePropertyClass<string>>()
                .Link(x => x.SecondValue).To(x => x.FirstValue)
                .Generate(2);

            Assert.That(builtItems.ElementAt(0).SecondValue, Is.EqualTo(builtItems.ElementAt(0).FirstValue));
            Assert.That(builtItems.ElementAt(1).SecondValue, Is.EqualTo(builtItems.ElementAt(1).FirstValue));
        }

        [Test]
        public void should_use_property_mapping_if_lookup_specified()
        {
            var builtItem = new FabricationScope().Setup<DoublePropertyClass<string>>()
                .For(x => x.FirstValue).PickFrom("A", "B", "C")
                .For(x => x.SecondValue).Map(x => x.FirstValue).Using(new Dictionary<string, string> { { "A", "MappedToA" }, { "B", "MappedToB" }, { "C", "MappedToC" } })
                .Generate();

            Assert.That(builtItem.SecondValue, Is.EqualTo("MappedTo" + builtItem.FirstValue));
        }

        [Test]
        public void should_use_property_mapping_if_modifier_specified()
        {
            var builtItem = new FabricationScope().Setup<DoublePropertyClass<string>>()
                .For(x => x.FirstValue).PickFrom("A", "B", "C")
                .For(x => x.SecondValue).Map(x => x.FirstValue).Using(x => "MappedTo" + x)
                .Generate();

            Assert.That(builtItem.SecondValue, Is.EqualTo("MappedTo" + builtItem.FirstValue));
        }

        [Test]
        public void should_ignore_declaration_order_when_property_mapping_specified()
        {
            var builtItem = new FabricationScope().Setup<DoublePropertyClass<string>>()
                .For(x => x.SecondValue).Map(x => x.FirstValue).Using(new Dictionary<string, string> { { "A", "MappedToA" }, { "B", "MappedToB" }, { "C", "MappedToC" } })
                .For(x => x.FirstValue).PickFrom("A", "B", "C")
                .Generate();

            Assert.That(builtItem.SecondValue, Is.EqualTo("MappedTo" + builtItem.FirstValue));
        }

        [Test]
        public void should_ignore_property_order_when_property_mapping_specified()
        {
            var builtItem = new FabricationScope().Setup<DoublePropertyClass<string>>()
                .For(x => x.FirstValue).Map(x => x.SecondValue).Using(new Dictionary<string, string> { { "A", "MappedToA" }, { "B", "MappedToB" }, { "C", "MappedToC" } })
                .For(x => x.SecondValue).PickFrom("A", "B", "C")
                .Generate();

            Assert.That(builtItem.FirstValue, Is.EqualTo("MappedTo" + builtItem.SecondValue));
        }

        [Test]
        public void should_fail_when_mapping_property_to_itself()
        {
            var options = new FabricationScope().Setup<DoublePropertyClass<string>>();

            Assert.Throws<LinkedPropertyException>(() => options.For(x => x.FirstValue).Map(x => x.FirstValue));
        }

        [Test]
        public void should_fail_when_linking_property_to_itself()
        {
            var options = new FabricationScope().Setup<DoublePropertyClass<string>>();

            Assert.Throws<LinkedPropertyException>(() => options.Link(x => x.FirstValue).To(x => x.FirstValue));
        }

        [Test]
        public void should_resolve_all_linked_properties_in_dependency_order()
        {
            var builtItem = new FabricationScope().Setup<AlpabeticalProperties>()
                .For(x => x.C).Use("Value")
                .Link(x => x.B).To(x => x.C)
                .For(x => x.A).Map(x => x.B).Using(new Dictionary<string, string> { { "Value", "MappedValue" }})
                .Generate();

            Assert.That(builtItem.C, Is.EqualTo("Value"));
            Assert.That(builtItem.B, Is.EqualTo("Value"));
            Assert.That(builtItem.A, Is.EqualTo("MappedValue"));
        }

        [Test]
        public void should_handle_when_a_property_is_the_source_of_multiple_link_rules()
        {
            var builtItem = new FabricationScope().Setup<AlpabeticalProperties>()
                .For(x => x.A).Use("Value")
                .Link(x => x.B).To(x => x.A)
                .Link(x => x.C).To(x => x.A)
                .Generate();

            Assert.That(builtItem.B, Is.EqualTo("Value"));
            Assert.That(builtItem.C, Is.EqualTo("Value"));
        }

        [Test]
        public void should_fail_when_a_property_is_the_target_of_multiple_link_rules()
        {
            var options = new FabricationScope().Setup<AlpabeticalProperties>()
                .For(x => x.A).Use("Value")
                .Link(x => x.B).To(x => x.A);

            Assert.Throws<LinkedPropertyException>(() => options.Link(x => x.B).To(x => x.C));
        }

        // TODO: Throw expception during setup
        [Test]
        public void should_fail_for_recursive_dependencies()
        {
            var options = new FabricationScope().Setup<AlpabeticalProperties>()
                .Link(x => x.A).To(x => x.B)
                .Link(x => x.B).To(x => x.C)
                .Link(x => x.C).To(x => x.A);

            var builtItem = options.Generate();
        }

        [Test]
        public void should_link_complex_properties()
        {
            var builtItem = new FabricationScope().Setup<DoublePropertyClass<InnerClass>>()
                .For(x => x.FirstValue).Use(new InnerClass { Name = "MyName" })
                .Link(x => x.SecondValue).To(x => x.FirstValue)
                .Generate();

            Assert.That(builtItem.SecondValue.Name, Is.EqualTo(builtItem.FirstValue.Name));
        }

        private class ClassWithStringAndEnum
        {
            public string Name { get; set; }
            public DayOfWeek DayOfWeek { get; set; }
        }

        [Test]
        public void should_handle_enum_properties_mapped_to_string_properties()
        {
            var builtItem = new FabricationScope().Setup<ClassWithStringAndEnum>()
                .For(x => x.Name).Use("Mon")
                .For(x => x.DayOfWeek).Map(x => x.Name).Using(x => x == "Mon" ? DayOfWeek.Monday : DayOfWeek.Tuesday)
                .Generate();

            Assert.That(builtItem.DayOfWeek, Is.EqualTo(DayOfWeek.Monday));
        }

        private class Names
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public string FullName { get; set; }
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_the_same_type()
        {
            var builtItem = new FabricationScope().Setup<Names>()
                .For(x => x.FirstName).Use("Adam")
                .For(x => x.LastName).Use("Jones")
                .For(x => x.FullName).Map(x => x.FirstName, x => x.LastName).Using((first,last) => first + " " + last)
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Adam Jones"));
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_the_same_type_when_first_is_null()
        {
            var builtItem = new FabricationScope().Setup<Names>()
                .For(x => x.FirstName).Use((string)null)
                .For(x => x.LastName).Use("Jones")
                .For(x => x.FullName).Map(x => x.FirstName, x => x.LastName).Using((first, last) => (first ?? "Unknown") + " " + last)
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Unknown Jones"));
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_the_same_type_when_second_is_null()
        {
            var builtItem = new FabricationScope().Setup<Names>()
                .For(x => x.FirstName).Use("Adam")
                .For(x => x.LastName).Use((string)null)
                .For(x => x.FullName).Map(x => x.FirstName, x => x.LastName).Using((first, last) => first + " " + (last ?? "Unknown"))
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Adam Unknown"));
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_the_same_type_when_both_are_null()
        {
            var builtItem = new FabricationScope().Setup<Names>()
                .For(x => x.FirstName).Use((string)null)
                .For(x => x.LastName).Use((string)null)
                .For(x => x.FullName).Map(x => x.FirstName, x => x.LastName).Using((first, last) => (first ?? "Unknown") + " " + (last ?? "Unknown"))
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Unknown Unknown"));
        }

        private class Names2
        {
            public string FirstName { get; set; }
            public int Number { get; set; }
            public string FullName { get; set; }
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_different_types()
        {
            var builtItem = new FabricationScope().Setup<Names2>()
                .For(x => x.FirstName).Use("Adam")
                .For(x => x.Number).Use(3)
                .For(x => x.FullName).Map(x => x.FirstName, x => x.Number).Using((first, number) => first + ":" + number)
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Adam:3"));
        }

        private class ClassWithString
        { 
            public string Value { get; set; }
            public override string ToString() { return Value; }
        }

        private class Names3
        {
            public string FirstName { get; set; }
            public ClassWithString Initial { get; set; }
            public string FullName { get; set; }
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_different_types_when_first_is_null()
        {
            var builtItem = new FabricationScope().Setup<Names3>()
                .For(x => x.FirstName).Use((string)null)
                .For(x => x.Initial).Use(new ClassWithString { Value = "Jones" })
                .For(x => x.FullName).Map(x => x.FirstName, x => x.Initial).Using((first, last) => (first ?? "Unknown") + ":" + last.Value)
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Unknown:Jones"));
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_different_types_when_second_is_null()
        {
            var builtItem = new FabricationScope().Setup<Names3>()
                .For(x => x.FirstName).Use("Adam")
                .For(x => x.Initial).Use((ClassWithString)null)
                .For(x => x.FullName).Map(x => x.FirstName, x => x.Initial).Using((first, last) => first + ":" + (last ?? new ClassWithString { Value="Unknown"}))
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Adam:Unknown"));
        }

        [Test]
        public void should_handle_properties_mapped_to_two_properties_of_different_types_when_both_are_null()
        {
            var builtItem = new FabricationScope().Setup<Names3>()
                .For(x => x.FirstName).Use((string)null)
                .For(x => x.Initial).Use((ClassWithString)null)
                .For(x => x.FullName).Map(x => x.FirstName, x => x.Initial).Using((first, last) => (first ?? "Unknown") + ":" + (last ?? new ClassWithString { Value = "Unknown" }))
                .Generate();

            Assert.That(builtItem.FullName, Is.EqualTo("Unknown:Unknown"));
        }
    }
}
